先上示例
1 | if (isSupported()) { |
1 |
|
代码解释
初始化
1 | setEGLContextClientVersion(2); //使用OpenGL ES 2.0 |
选择EGL配置
Android设备往往支持多种EGL配置,可以使用不同数目的通道(channel),也可以指定每个通道具有不同数目的位(bits)深度。因此, 在渲染器工作之前就应该指定EGL的配置。GLSurfaceView默认EGL配置的像素格式为RGB_656,16位的深度缓存(depth buffer),默认不开启遮罩缓存(stencil buffer)。
1 | surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); |
设置渲染模式
RenderMode 有两种,RENDERMODE_WHEN_DIRTY
和 RENDERMODE_CONTINUOUSLY
,前者是懒惰渲染,需要手动调用 glSurfaceView.requestRender()
才会进行更新,而后者则是不停渲染。
1 | surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); |
GLSL 程序
和普通的 view 利用 canvas 来绘制不一样,OpenGL 需要加载 GLSL 程序,让 GPU 进行绘制。所以我们需要定义 shader 代码,并在初始化时(也就是 onSurfaceCreated
回调中)加载。
- 创建 GLSL 程序:
glCreateProgram
在连接shader之前,首先要创建一个容纳程序的容器,称为着色器程序容器。
- 加载 shader 代码
glCreateShader
:创建一个着色器对象,指定要创建的着色器的类型。 只能是 GL_VERTEX_SHADER 或 GL_FRAGMENT_SHADER。**GL_VERTEX_SHADER **类型的着色器是一个用于在可编程顶点处理器上运行的着色器。 GL_FRAGMENT_SHADER 类型的着色器是一个着色器
glShaderSource
:替换着色器对象中的源代码,glShaderSource 将着色器中的源代码设置为string指定的字符串数组中的源代码。先前存储在着色器对象中的任何源代码都将被完全替换。
glCompileShader
:编译一个着色器对象,**glCompileShader 编译已存储在shader指定的着色器对象中的源代码字符串。
- attatchshader 代码:
glAttachShader
将着色器对象附加到program对象。
- 链接 GLSL 程序:
glLinkProgram
连接一个program对象。**glLinkProgram 链接program指定的program对象。附加到program的类型为GL_VERTEX_SHADER的着色器对象用于创建将在可编程顶点处理器上运行的可执行文件。 附加到program的类型为GL_FRAGMENT_SHADER的着色器对象用于创建将在可编程片段处理器上运行的可执行文件。
- 使用 GLSL 程序:
glUseProgram
使用程序对象作为当前渲染状态的一部分。通过使用 glAttachShader 成功将着色器对象附加到程序对象,使用 glCompileShader 成功编译着色器对象,并成功链接程序对象与 glLinkProgram,之后就可以在程序对象中创建每个阶段的可执行文件。
- 获取 shader 代码中的变量索引:
glGetAttribLocation
返回属性变量的位置。glGetAttribLocation 查询由program指定的先前链接的程序对象,用于name指定的属性变量,并返回绑定到该属性变量的通用顶点属性的索引。 如果name是矩阵属性变量,则返回矩阵的第一列的索引。 如果指定的属性变量不是指定程序对象中的活动属性,或者名称以保留前缀“gl_”开头,则返回-1。
- 启用 vertex:
glEnableVertexAttribArray
启用或禁用通用顶点属性数组。将访问通用顶点属性数组中的值,并在调用顶点数组命令( 如 glDrawArrays 或 glDrawElements )时用于呈现。
- 绑定 vertex 坐标值:
glVertexAttribPointer
glVertexAttribPointer 指定索引index处的通用顶点属性数组的位置和数据格式,以便在渲染时使用。
ndex:指定要修改的通用顶点属性的索引。
size:指定每个通用顶点属性的组件数。 必须为1,2,3或4.初始值为4。
type:指定数组中每个组件的数据类型。 接受符号常量GL_BYTE,GL_UNSIGNED_BYTE,GL_SHORT,GL_UNSIGNED_SHORT,GL_FIXED 或 GL_FLOAT。 初始值为GL_FLOAT。
normalized:指定在访问定点数据值时是应将其标准化(GL_TRUE)还是直接转换为定点值(GL_FALSE)。
stride:指定连续通用顶点属性之间的字节偏移量。 如果stride为0,则通用顶点属性被理解为紧密打包在数组中的。 初始值为0。
pointer:指定指向数组中第一个通用顶点属性的第一个组件的指针。 初始值为0。
绘制
1 | //绘制顶点 |
投影变换
OpenGL 坐标系和安卓手机坐标系不是线性对应的,因为手机的宽高比几乎都不是 1。因此我们绘制的形状是变形的。要解决这个问题就可以使用投影变换。
使用较多的是正投影和透视投影,这里我们使用透视投影:Matrix.perspectiveM
。通常坐标系的变换都是对顶点坐标进行矩阵左乘运算,因此我们需要修改我们的 vertex shader :
1 | private static final String VERTEX_SHADER = |
在 onSurfaceCreated
中获取 uMVPMatrix
的索引:
1 |
|
在 onSurfaceChanged
中计算变换矩阵:
1 | private float[] mMVPMatrix = new float[16]; |
最后我们在绘制时为 uMVPMatrix
赋值:
1 |
|
使用的顶点数据:
1 | //顶点数组 |
未变换前,(1,0,0)在屏幕中间最右边,(1,1,0)在右上角。变换后,(1,1,0)在右边,与
(1,0,0)的距离跟(1,0,0)到(0,0,0)一样,即坐标系跟手机实际尺寸比例相等。
1 | public static void perspectiveM(float[] m, int offset, |
m
是保存变换矩阵的数组,offset
是开始保存的下标偏移量。fovy 是 y 轴的 field of view 值,也就是视角大小,视角越大,我们看到的范围就越广,例如 90° 和 45°, 90° 整个图像就会比45°小一点。aspect
是 Screen space 的宽高比。zNear
和 zFar
则是视锥体近平面和远平面的 z 轴坐标了。
由于历史原因,Matrix.perspectiveM
会让 z 轴方向倒置,所以左乘投影矩阵之后,顶点 z 坐标需要在 -zNear~-zFar
范围内才会可见。
前面我们顶点的 z 坐标都是 0,我们可以把它修改为 -0.1f~-100f
之间的值,也可以通过一个位移变换来达到此目的:
1 | Matrix.translateM(mMVPMatrix, 0, 0f, 0f, -2.5f); |
我们沿着 z 轴的反方向移动 2.5,这样就能把 z 坐标移到 -0.1f~-100f
了。